﻿//-----------------------------------------------------------------------
// <copyright file="Formatter.cs" company="Rhys Parry">
//    Copyright Rhys Parry 2008
//    This file is released under the Apache 2.0 License
// </copyright>
//-----------------------------------------------------------------------
namespace Smarts.Core
{
    using System;
    using System.Text;
    using System.Text.RegularExpressions;

    /// <summary>
    /// Provides a number of methods for specialised formatting
    /// </summary>
    public static class Formatter
    {
        /// <summary>
        /// A constant for 1 billion (1,000,000,000)
        /// </summary>
        public const int OneBillion = 1000000000;

        /// <summary>
        /// A constant for 1 million (1,000,000)
        /// </summary>
        public const int OneMillion = 1000000;

        /// <summary>
        /// A constant for 1 thousand (1,000)
        /// </summary>
        public const int OneThousand = 1000;

        /// <summary>
        /// A constant for 1 hundred
        /// </summary>
        public const int OneHundred = 100;

        /// <summary>
        /// The word form for the first thirteen unique
        /// number words which form the basis for wordy
        /// numbers (starting at zero).
        /// </summary>
        private static readonly string[] BaseNumbers = 
        {
            "zero",
            "one",
            "two",
            "three",
            "four",
            "five",
            "six",
            "seven",
            "eight",
            "nine",
            "ten",
            "eleven",
            "twelve"
        };

        /// <summary>
        /// Returns the full word form of <paramref name="number"/>
        /// </summary>
        /// <param name="number">The number to turn into words</param>
        /// <returns>The number in full word form</returns>
        public static string NumberWords(int number)
        {
            if (number < 0)
            {
                return string.Format("minus {0}", NumberWords(Math.Abs(number)));
            }

            // only need to technically support up to 2,147,483,647
            if (number >= OneBillion)
            {
                return BreakDownNumber(number, OneBillion, "billion");
            }

            if (number >= OneMillion)
            {
                return BreakDownNumber(number, OneMillion, "million");
            }
            
            if (number >= OneThousand)
            {
                return BreakDownNumber(number, OneThousand, "thousand");
            }
            
            if (number >= OneHundred)
            {
                return BreakDownNumber(number, OneHundred, "hundred");
            }

            int ones;
            if (number >= 20)
            {
                var tens = number / 10;
                ones = number % 10;
                return ones > 0 ? TenPrefix(tens) + "ty-" + NumberWords(ones) : TenPrefix(tens) + "ty";
            }

            if (number < 13)
            {
                return BaseNumbers[number];
            }

            ones = number % 10;
            return string.Format("{0}teen", TenPrefix(ones));
        }

        /// <summary>
        /// Returns the full word form of <paramref name="number"/>
        /// up to and including <paramref name="limit"/>
        /// </summary>
        /// <param name="number">The number to turn into words</param>
        /// <param name="limit">The point at which the number should be expressed numerically</param>
        /// <returns>The full word version of the number up to and including limit</returns>
        public static string NumberWords(int number, int limit)
        {
            return number > limit ? number.ToString() : NumberWords(number);
        }

        /// <summary>
        /// Returns the full word form of <paramref name="number"/>
        /// up to and including <paramref name="limit"/>
        /// </summary>
        /// <param name="number">The number to turn into words</param>
        /// <param name="limit">The point at which the number should be expressed numerically</param>
        /// <returns>The full word version of the number up to and including limit</returns>
        public static string NumberWords(double number, int limit)
        {
            return number > limit ? number.ToString() : NumberWords(number);
        }

        /// <summary>
        /// Returns the full word form of <paramref name="number"/>
        /// </summary>
        /// <param name="number">The number to turn into words</param>
        /// <returns>The number in full word form</returns>
        public static string NumberWords(double number)
        {
            var whole = (int)number;
            if (whole - number == 0)
            {
                return NumberWords(whole);
            }
            
            var numberWord = new StringBuilder(NumberWords(whole));
            numberWord.Append(" point");
            var points = number.ToString().Split('.')[1];
            foreach (var c in points)
            {
                numberWord.Append(" ");
                numberWord.Append(NumberWords(int.Parse(c.ToString())));
            }

            return numberWord.ToString();
        }

        /// <summary>
        /// Returns the string with numbers replaced with words
        /// </summary>
        /// <param name="textWithNumbers">The text to search</param>
        /// <returns>The provided text with all numbers substituted with their full word form</returns>
        public static string NumberWords(string textWithNumbers)
        {
            return textWithNumbers == null ? null : NumberWords(textWithNumbers, -1);
        }

        /// <summary>
        /// Returns the string with numbers replaced with words up to the limit
        /// </summary>
        /// <param name="textWithNumbers">The text to search</param>
        /// <param name="limit">The maximum value to turn into words</param>
        /// <returns>The provided text with all numbers substituted with their full word form</returns>
        public static string NumberWords(string textWithNumbers, int limit)
        {
            if (textWithNumbers == null)
            {
                return null;
            }

            // First see if a number exists in the string
            var numeric = new Regex(@"[-+]?(([1-9][0-9]+)|[0-9])(\.[0-9]+)?");
            if (!numeric.IsMatch(textWithNumbers))
            {
                return textWithNumbers;
            }

            // split string out
            var extractedNumber = numeric.Match(textWithNumbers);
            var beforeTheNumber = textWithNumbers.Substring(0, extractedNumber.Index);
            var afterTheNumber = textWithNumbers.Substring(extractedNumber.Index + extractedNumber.Length);
            return limit == -1
                       ? beforeTheNumber + NumberWords(double.Parse(extractedNumber.Value)) +
                         NumberWords(afterTheNumber)
                       : beforeTheNumber + NumberWords(double.Parse(extractedNumber.Value), limit) +
                         NumberWords(afterTheNumber, limit);
        }

        /// <summary>
        /// Divides a large number into smaller more manageable bits for
        /// turning into word form
        /// </summary>
        /// <param name="number">The number to break down</param>
        /// <param name="breakBy">The degree to break by (e.g. 1000)</param>
        /// <param name="breakWord">The string to insert between the broken portions</param>
        /// <returns>The number with its specific portions broken down</returns>
        private static string BreakDownNumber(int number, int breakBy, string breakWord)
        {
            var breakinto = number / breakBy;
            var remaining = number % breakBy;
            if (remaining >= 100)
            {
                return string.Format("{0} {1} {2}", NumberWords(breakinto), breakWord, NumberWords(remaining));
            }

            return remaining > 0
                       ? string.Format("{0} {1} and {2}", NumberWords(breakinto), breakWord, NumberWords(remaining))
                       : string.Format("{0} {1}", NumberWords(breakinto), breakWord);
        }

        /// <summary>
        /// Calculates the prefix for a number when it is used
        /// in a composite form
        /// </summary>
        /// <param name="number">The number to find the prefix for</param>
        /// <returns>The prefix for the number (e.g. 3 = thir)</returns>
        private static string TenPrefix(int number)
        {
            switch (number)
            {
                case 9:
                case 7:
                case 6:
                case 4:
                    return NumberWords(number);
                case 8:
                    return "eigh";
                case 5:
                    return "fif";
                case 3:
                    return "thir";
                case 2:
                    return "twen";
                default:
                    throw new ArgumentOutOfRangeException("number", "The number provided is not supported");
            }
        }
    }
}
